# encoding: utf-8
# @Author: ShiMingliang
# @Date  :  2025/08/04
from openpyxl import load_workbook
from typing import List, Tuple, Any     # 导入os模块，用于处理文件路径和系统相关操作
import os


# 定义一个Excel数据读取的基类，使用class关键字声明类
class ExcelDataReader:
    """优化后的Excel数据读取基类，支持动态列数检测"""

    # 类变量，用于缓存已加载的workbook对象，初始化为None
    _workbook = None
    # 类变量，定义Excel文件的默认路径，使用os.path相关方法处理路径
    _file_path =r'D:\codo\PythonProject\test0902\case_file\login_demo.xlsx'
    # 类变量，字典类型，用于缓存每个sheet的列数，避免重复计算
    _sheet_columns = {}

    # 类方法，使用@classmethod装饰器声明，可通过类名直接调用
    @classmethod
    def _get_workbook(cls):  # cls参数代表类本身
        """单例模式获取workbook"""
        # 检查_workbook是否为None，如果是则加载Excel文件
        if cls._workbook is None:
            # 检查文件是否存在
            if not os.path.exists(cls._file_path):
                # 如果不存在则抛出FileNotFoundError异常
                raise FileNotFoundError(f"Excel文件不存在: {cls._file_path}")
            # 加载Excel文件，data_only=True表示获取公式计算后的值
            cls._workbook = load_workbook(cls._file_path, data_only=True)
        # 返回_workbook对象
        return cls._workbook

    # 另一个类方法，用于获取指定sheet的有效列数
    @classmethod
    def _get_sheet_columns(cls, sheet_name: str) -> int:  # 参数sheet_name为字符串类型，返回整数
        """获取sheet的有效列数（基于第一行标题）"""
        # 检查是否已缓存该sheet的列数
        if sheet_name not in cls._sheet_columns:
            # 获取workbook对象
            wb = cls._get_workbook()
            # 检查sheet是否存在
            if sheet_name not in wb.sheetnames:
                # 不存在则抛出ValueError异常
                raise ValueError(f"工作表 '{sheet_name}' 不存在")

            # 获取指定的sheet对象
            sheet = wb[sheet_name]
            # 使用iter_rows方法获取第一行数据，values_only=True表示只获取值
            # min_row=1, max_row=1表示只读取第一行
            header_row = next(sheet.iter_rows(min_row=1, max_row=1, values_only=True))

            # 初始化列计数器
            col_count = 0
            # 遍历第一行的每个单元格
            for cell in header_row:
                # 如果单元格为None或空字符串，则停止计数
                if cell is None or str(cell).strip() == "":
                    break
                # 否则列数加1
                col_count += 1

            # 检查是否至少有一列有效数据
            if col_count == 0:
                raise ValueError(f"工作表 '{sheet_name}' 的标题行没有有效列")

            # 缓存该sheet的列数
            cls._sheet_columns[sheet_name] = col_count

        # 返回缓存的列数
        return cls._sheet_columns[sheet_name]

    # 通用数据读取方法
    @classmethod
    def read_data(cls, sheet_name: str) -> List[Tuple[Any, ...]]:  # 返回元组列表，每个元组包含任意类型数据

        """
        通用数据读取方法，自动适应列数
        :param sheet_name: 工作表名称
        :return: 测试数据列表，每行是一个元组，包含对应列的数据
        """
        try:
            # 获取指定sheet的列数
            col_count = cls._get_sheet_columns(sheet_name)
            # 获取workbook对象
            wb = cls._get_workbook()
            # 获取指定的sheet对象
            sheet = wb[sheet_name]

            # 初始化数据列表
            data = []
            # 遍历sheet的每一行，从第2行开始（min_row=2），values_only=True表示只获取值
            for row in sheet.iter_rows(min_row=2, values_only=True):
                # 初始化处理后的行数据列表
                processed_row = []
                # 遍历每一列（根据之前获取的列数）
                for i in range(col_count):
                    # 获取单元格值，如果索引超出范围则为None
                    cell_value = row[i] if i < len(row) else None
                    # 处理单元格值：如果为None或空字符串则转为None，否则保留原值
                    processed_row.append(cell_value if cell_value is not None and str(cell_value).strip() != "" else None)

                # 检查该行是否有至少一个非空单元格
                if any(cell is not None for cell in processed_row):
                    # 将处理后的行转为元组并添加到数据列表中
                    data.append(tuple(processed_row))

            # 返回数据列表
            return data

        # 捕获所有异常
        except Exception as e:
            # 抛出新的异常，包含原始异常信息
            raise Exception(f"读取Excel数据失败: {str(e)}")


# 定义一个专门用于读取登录测试数据的类，继承自ExcelDataReader
class LoginTestData(ExcelDataReader):
    """登录测试数据读取类"""

    # 类方法，获取登录成功测试数据
    @classmethod
    def get_success_data(cls) -> List[Tuple[Any, ...]]:
        """获取登录成功测试数据"""
        # 调用父类的read_data方法，读取"登录成功"sheet的数据
        return cls.read_data("登录成功")

    # 类方法，获取用户名错误测试数据
    @classmethod
    def get_username_error_data(cls) -> List[Tuple[Any, ...]]:
        """获取用户名错误测试数据"""
        # 调用父类的read_data方法，读取"账号错误"sheet的数据
        return cls.read_data("账号错误")

    # 类方法，获取密码错误测试数据
    @classmethod
    def get_password_error_data(cls) -> List[Tuple[Any, ...]]:
        """获取密码错误测试数据"""
        # 调用父类的read_data方法，读取"密码错误"sheet的数据
        return cls.read_data("密码错误")


if __name__ == "__main__":
    print(LoginTestData.get_success_data())
    print(LoginTestData.get_username_error_data())
    print(LoginTestData.get_password_error_data())
"""
解释：
def get_password_error_data(cls) -> List[Tuple[Any, ...]]:
1. -> 符号
表示函数返回值的类型注解。
语法：def 函数名(参数) -> 返回类型:


2. List[Tuple[Any, ...]] 的含义
这是一个嵌套的类型提示，分解如下：
List
表示返回的是一个列表（来自typing.List）。
Tuple[Any, ...]
表示列表中的每个元素是一个元组（来自typing.Tuple），元组的结构是：
Any：元组中的每个元素可以是任意类型（来自typing.Any）。
...（省略号）：表示元组的长度可变（即元组可以有任意数量的Any类型元素）。


3. 为什么用这种复杂的类型注解？
函数 get_password_error_data() 调用 read_data()，而 read_data() 返回的是 List[Tuple[Any, ...]]（例如：[(数据1, 数据2), (数据1, 数据2), ...]）。
这种注解明确说明了：
返回值是一个列表。
列表中的每个元素是一个元组。
元组中的元素可以是任意类型（如字符串、数字、None等）。
元组的长度不固定（取决于Excel的列数）。


4. 对比无类型注解的版本
无类型注解（动态类型，Python默认）
def get_password_error_data(cls):
    return cls.read_data("密码错误")
有类型注解（更清晰，便于静态检查）
def get_password_error_data(cls) -> List[Tuple[Any, ...]]:
    return cls.read_data("密码错误")



补充说明
Any vs 具体类型
如果明确知道元组中数据的类型（例如全是字符串），可以更精确地写成：
-> List[Tuple[str, ...]]  # 表示元组中所有元素是字符串
但这里用 Any 是因为Excel数据可能是混合类型（如数字、字符串、空值等）。

... 的含义
Tuple[Any, ...] 等价于 Tuple[Any, Any, Any, ...]，表示元组长度可变。如果元组长度固定（例如总是2列），可以写成：
-> List[Tuple[Any, Any]]  # 每个元组有且只有2个任意类型的元素
"""